﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Controls;
using Notoric.Commanding.CommandBase;
using Notoric.Gui.Windows;

namespace Notoric.Gui
{
    /// <summary>
    /// Controls the active command stacks and verifies the temporal dependcy constraint.
    /// </summary>
    public class CommandStackManager : INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="CommandStackManager"/> instance.
        /// </summary>
        /// <param name="mainWindow">
        /// <para>
        /// Type: <see cref="MainWindow"/>
        /// </para>
        /// <para>
        /// The main application window.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="mainWindow"/> is <see langword="null"/>.
        /// </exception>
        public CommandStackManager(MainWindow mainWindow)
        {
            if (mainWindow == null)
                throw new ArgumentNullException("mainWindow");

            this.mainWindow = mainWindow;
            activeUndoStacks = new List<CommandStack>();
            activeRedoStacks = new List<CommandStack>();

            CommandStack undoStack, redoStack;
            foreach (AvalonDock.DocumentContent document in mainWindow.DocumentPane.Items)
            {
                GetStacks(document, out undoStack, out redoStack);
                activeUndoStacks.Add(undoStack);
                activeRedoStacks.Add(redoStack);
                undoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(undoStack_ItemsChanged);
                redoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(redoStack_ItemsChanged);
            }

            mainWindow.ProjectExplorer.Projects.CollectionChanged += new NotifyCollectionChangedEventHandler(Projects_CollectionChanged);
            mainWindow.DocumentPane.SelectionChanged += new SelectionChangedEventHandler(DocumentPane_SelectionChanged);
            ((INotifyCollectionChanged)mainWindow.DocumentPane.Items).CollectionChanged += new NotifyCollectionChangedEventHandler(Documents_CollectionChanged);
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <inheritdoc/>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the collection of open documents has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Documents_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            CommandStack undoStack, redoStack;

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (AvalonDock.DocumentContent document in e.NewItems)
                    {
                        GetStacks(document, out undoStack, out redoStack);
                        activeUndoStacks.Add(undoStack);
                        activeRedoStacks.Add(redoStack);
                        undoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(undoStack_ItemsChanged);
                        redoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(redoStack_ItemsChanged);
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (AvalonDock.DocumentContent document in e.OldItems)
                    {
                        GetStacks(document, out undoStack, out redoStack);
                        activeUndoStacks.Remove(undoStack);
                        activeRedoStacks.Remove(redoStack);
                        undoStack.ItemsChanged -= undoStack_ItemsChanged;
                        redoStack.ItemsChanged -= redoStack_ItemsChanged;

                        undoStack.Clear();
                        redoStack.Clear();
                    }
                    break;
                case NotifyCollectionChangedAction.Reset:
                    foreach (var stack in activeRedoStacks)
                    {
                        stack.ItemsChanged -= redoStack_ItemsChanged;
                        stack.Clear();
                    }
                    foreach (var stack in activeUndoStacks)
                    {
                        stack.ItemsChanged -= undoStack_ItemsChanged;
                        stack.Clear();
                    }
                    activeUndoStacks.Clear();
                    activeRedoStacks.Clear();
                    break;
            }
        }

        /// <summary>
        /// Occurs when the collection of commands on a redo stack has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void redoStack_ItemsChanged(object sender, ItemsChangedEventArgs e)
        {
            if (sender == activeElementRedoStack || sender == activeProjectRedoStack)
                SetActiveStacks();
            else
                CheckCanExecute();
        }

        /// <summary>
        /// Occurs when the collection of commands on an undo stack has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void undoStack_ItemsChanged(object sender, ItemsChangedEventArgs e)
        {
            if (sender == activeElementUndoStack || sender == activeProjectUndoStack)
                SetActiveStacks();
            else
                CheckCanExecute();
        }

        /// <summary>
        /// Occurs when the active document has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void DocumentPane_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (mainWindow.DocumentPane.SelectedItem != null)
                GetStacks(mainWindow.DocumentPane.SelectedItem as AvalonDock.DocumentContent, out activeElementUndoStack, out activeElementRedoStack);
            else
            {
                activeElementRedoStack = null;
                activeElementUndoStack = null;
            }

            SetActiveStacks();
        }

        /// <summary>
        /// Occurs when the collection of open projects has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Projects_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (mainWindow.ProjectExplorer.Projects.Count > 0)
            {
                activeProjectRedoStack = mainWindow.ProjectExplorer.Projects[0].RedoStack;
                activeProjectUndoStack = mainWindow.ProjectExplorer.Projects[0].UndoStack;

                activeProjectRedoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(redoStack_ItemsChanged);
                activeProjectUndoStack.ItemsChanged += new EventHandler<ItemsChangedEventArgs>(undoStack_ItemsChanged);
            }
            else
            {
                activeProjectUndoStack.ItemsChanged -= undoStack_ItemsChanged;
                activeProjectRedoStack.ItemsChanged -= redoStack_ItemsChanged;

                activeProjectUndoStack = null;
                activeProjectRedoStack = null;
            }

            SetActiveStacks();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Sets the <see cref="CanUndo"/> and <see cref="CanRedo"/> properties.
        /// </summary>
        private void CheckCanExecute()
        {
            if (ActiveUndoStack == null || ActiveUndoStack.IsEmpty)
                CanUndo = false;
            else
            {
                CanUndo = true;

                var command = ActiveUndoStack.Peek();
                foreach (var stack in activeUndoStacks)
                {
                    if (stack != ActiveUndoStack && !stack.IsEmpty && stack.Peek().LastExecuted.CompareTo(command.LastExecuted) > 0)
                    {
                        CanUndo = false;
                        break;
                    }
                }
            }

            if (ActiveRedoStack == null || ActiveRedoStack.IsEmpty)
                CanRedo = false;
            else
            {
                CanRedo = true;

                var command = ActiveRedoStack.Peek();
                foreach (var stack in activeRedoStacks)
                {
                    if (stack != ActiveRedoStack && !stack.IsEmpty && stack.Peek().LastExecuted.CompareTo(command.LastExecuted) > 0)
                    {
                        CanRedo = false;
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the command stacks associated with a given document.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="undoStack">Document's undo stack.</param>
        /// <param name="redoStack">Document's redo stack.</param>
        private void GetStacks(AvalonDock.DocumentContent document, out CommandStack undoStack, out CommandStack redoStack)
        {
            if (document is PagePanel)
            {
                var page = ((PagePanel)document).Page;
                undoStack = page.UndoStack;
                redoStack = page.RedoStack;
            }
            else if (document is LinearCompositionViewPanel)
            {
                var linearView = (ViewModel.Elements.LinearCompositionView)document.DataContext;
                undoStack = linearView.UndoStack;
                redoStack = linearView.RedoStack;
            }
            else
                throw new ArgumentException("Unknown document type (" + document.GetType().FullName + ").", "document");
        }

        /// <summary>
        /// Sets the <see cref="ActiveUndoStack"/> and <see cref="ActiveRedoStack"/> properties.
        /// </summary>
        private void SetActiveStacks()
        {
            if (activeElementUndoStack == null)
                ActiveUndoStack = activeProjectUndoStack;
            else if (activeProjectUndoStack.IsEmpty || (!activeElementUndoStack.IsEmpty && activeElementUndoStack.Peek().LastExecuted.CompareTo(activeProjectUndoStack.Peek().LastExecuted) > 0))
                ActiveUndoStack = activeElementUndoStack;
            else
                ActiveUndoStack = activeProjectUndoStack;

            if (activeElementRedoStack == null)
                ActiveRedoStack = activeProjectRedoStack;
            else if (activeProjectRedoStack.IsEmpty || (!activeElementRedoStack.IsEmpty && activeElementRedoStack.Peek().LastExecuted.CompareTo(activeProjectRedoStack.Peek().LastExecuted) > 0))
                ActiveRedoStack = activeElementRedoStack;
            else
                ActiveRedoStack = activeProjectRedoStack;

            if (ActiveUndoStack != null && !ActiveUndoStack.IsEmpty)
                ActiveUndoCommand = ActiveUndoStack.Peek();
            else
                ActiveUndoCommand = null;
            if (ActiveRedoStack != null && !ActiveRedoStack.IsEmpty)
                ActiveRedoCommand = ActiveRedoStack.Peek();
            else
                ActiveRedoCommand = null;

            CheckCanExecute();
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the command that will be executed on next redo.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The command that will be redone by the next redo operation.<br/>
        /// This value is <see langword="null"/> if the <see cref="ActiveRedoStack"/> is empty.
        /// </para>
        /// </value>
        public UndoableCommandBase ActiveRedoCommand
        {
            get { return activeRedoCommand; }
            private set
            {
                activeRedoCommand = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("ActiveRedoCommand"));
            }
        }

        /// <summary>
        /// Gets or sets the command that will be executed on next undo.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The command that will be undone by the next undo operation.<br/>
        /// This value is <see langword="null"/> if the <see cref="ActiveUndoStack"/> is empty.
        /// </para>
        /// </value>
        public UndoableCommandBase ActiveUndoCommand
        {
            get { return activeUndoCommand; }
            private set
            {
                activeUndoCommand = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("ActiveUndoCommand"));
            }
        }

        /// <summary>
        /// Gets the redo stack that is currently active.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="CommandStack"/>
        /// </para>
        /// <para>
        /// The redo command stack that is currently active
        /// (i.e. Redo executes a command from the top of this stack).
        /// </para>
        /// </value>
        public CommandStack ActiveRedoStack
        {
            get { return activeRedoStack; }
            private set
            {
                activeRedoStack = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("ActiveRedoStack"));
            }
        }

        /// <summary>
        /// Gets the undo stack that is currently active.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="CommandStack"/>
        /// </para>
        /// <para>
        /// The undo command stack that is currently active
        /// (i.e. Undo executes a command from the top of this stack).
        /// </para>
        /// </value>
        public CommandStack ActiveUndoStack
        {
            get { return activeUndoStack; }
            private set
            {
                activeUndoStack = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("ActiveUndoStack"));
            }
        }

        /// <summary>
        /// Gets the value indicating whether Redo can be currently executed or not.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="bool"/>
        /// </para>
        /// <para>
        /// Indicates whether the Redo operation can be currently executed or not.<br/>
        /// The operation may not be possible for one of the three following reasons:
        /// <list type="number">
        /// <item>
        /// The <see cref="ActiveRedoStack"/> is <see langword="null"/> (i.e. no document and no project is open).
        /// </item>
        /// <item>
        /// The <see cref="ActiveRedoStack"/> is empty (there is no command to redo).
        /// </item>
        /// <item>
        /// There is an open document that has a newer (i.e. executed later) command on its redo stack.
        /// </item>
        /// </list>
        /// </para>
        /// </value>
        public bool CanRedo
        {
            get { return canRedo; }
            private set
            {
                canRedo = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("CanRedo"));
            }
        }

        /// <summary>
        /// Gets the value indicating whether Undo can be currently executed or not.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="bool"/>
        /// </para>
        /// <para>
        /// Indicates whether the Undo operation can be currently executed or not.<br/>
        /// The operation may not be possible for one of the three following reasons:
        /// <list type="number">
        /// <item>
        /// The <see cref="ActiveUndoStack"/> is <see langword="null"/> (i.e. no document and no project is open).
        /// </item>
        /// <item>
        /// The <see cref="ActiveUndoStack"/> is empty (there is no command to redo).
        /// </item>
        /// <item>
        /// There is an open document that has a newer (i.e. executed later) command on its undo stack.
        /// </item>
        /// </list>
        /// </para>
        /// </value>
        public bool CanUndo
        {
            get { return canUndo; }
            private set
            {
                canUndo = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("CanUndo"));
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the redo stack of the active document.
        /// </summary>
        private CommandStack activeElementRedoStack;
        /// <summary>
        /// Holds the undo stack of the active document.
        /// </summary>
        private CommandStack activeElementUndoStack;
        /// <summary>
        /// Holds the redo stack of the active project.
        /// </summary>
        private CommandStack activeProjectRedoStack;
        /// <summary>
        /// Holds the undo stack of the active project.
        /// </summary>
        private CommandStack activeProjectUndoStack;
        /// <summary>
        /// Holds the active redo command.
        /// </summary>
        private UndoableCommandBase activeRedoCommand;
        /// <summary>
        /// Holds the active undo command.
        /// </summary>
        private UndoableCommandBase activeUndoCommand;
        /// <summary>
        /// Holds the value of the <see cref="ActiveUndoStack"/> property.
        /// </summary>
        private CommandStack activeUndoStack;
        /// <summary>
        /// Holds the value of the <see cref="ActiveRedoStack"/> property.
        /// </summary>
        private CommandStack activeRedoStack;
        /// <summary>
        /// Holds the list of controlled redo stacks.
        /// </summary>
        private List<CommandStack> activeRedoStacks;
        /// <summary>
        /// Holds the list of controlled undo stacks.
        /// </summary>
        private List<CommandStack> activeUndoStacks;
        /// <summary>
        /// Holds the value of the <see cref="CanRedo"/> property.
        /// </summary>
        private bool canRedo;
        /// <summary>
        /// Holds the value of the <see cref="CanUndo"/> property.
        /// </summary>
        private bool canUndo;
        /// <summary>
        /// Holds a reference to the main application window.
        /// </summary>
        private MainWindow mainWindow;

        #endregion
    }
}
